# encoding: UTF-8

"""
本文件包含了策略开发用类模板，开发策略时需要继承 IStrategy 或者 StrategyTemplate 类。
策略只需根据历史数据和即时数据计算相关指标、交易信号（开多、平多、开空、平空，也可能有止损价位），
并将得到的交易信号送到相关引擎（策略引擎、测试引擎、回测引擎）的事件中，供风险引擎调用。
该类包含的重要属性和方法：
属性：
paramNameList

方法：

"""
import multiprocessing
import datetime

from engine.strategy_engine import *
from indicator.indicators import *
from utils.function import str_datetime, today_date

# 开仓方向
OPEN_DIRECTION_LONG = 'LONG'
OPEN_DIRECTION_SHORT = 'SHORT'
OPEN_DIRECTION_BOTH = 'BOTH'


class IStrategy(object):
    """策略模板
    provide the following methods:
    on_init: 初始化策略，必须由具体策略继承实现,并要调用此父函数
        在此可以调入参数，优化参数，设置止损方法，调入历史数据、计算相关指标
        由策略引擎调用
    on_start: 注册相关事件()，由策略引擎调用，
    on_stop: 注销相关事件，由策略引擎调用
    on_tick: 收到Tick数据时执行
    on_ticks: 收到历史Tick数据序列时执行
    on_bar: 收到K线数据时执行
    on_bars: 收到历史K线数据序列时执行
    on_optimize:

    req_bars
    req_ticks
    req_params

    ===============
    buy: open long
    sell: close long
    short: open short
    cover: close short
    _sendOrder: send the order to trade-server or backtest engine
    _cancelOrder: cancel order

    """

    @classmethod
    def calc_long_signal(cls, data, params):
        """
        类级方法：计算做多信号,当做多头回测时调用，由具体策略实现
        :param data:
        :param params:参数字典：{参数名称：参数值}，包含'period':PERIOD_TYPE_TICK
        :return:pd.DataFrame(columns=['tprice','buy','sell'],index='datetime')
        """
        print('IStrategy.calc_long_signal')
        return None

    @classmethod
    def calc_short_signal(cls, data, params):
        """
        类级方法：计算做空信号,当做空头回测时调用，由具体策略实现
        :param data:
        :param params:
        :return:pd.DataFrame(columns=['tprice','short','cover'],index='datetime')
        """
        print('IStrategy.calc_short_signal')
        return None

    @classmethod
    def calc_long_short_signal(cls, data, params):
        """
        类级方法：计算多空信号，当做多、空回测时调用，由具体策略实现
        :param data:
        :param params:
        :return:pd.DataFrame(columns=['tprice','buy','sell','short','cover'],index='datetime')
        """
        print('IStrategy.calc_long_short_signal')
        rslt = {}
        rslt['long'] = cls.calc_long_signal(data, params)
        rslt['short'] = cls.calc_short_signal(data, params)
        return rslt

    @classmethod
    def run_param_optimization(cls, data, params):
        """
        类级方法：参数优化
        :param data: 供优化的数据
        :param params:{name:{default:1,min:1,max:2,step:1,multiplier:1,description:''}}
        {name:[min,max,step]}
        :return:
        """

    # 策略类的名称和作者
    strategy_class_name = "IStrategy"
    author = EMPTY_UNICODE

    _type = FORMULA_TYPE_STRATEGY

    #
    # 策略的基本参数
    instance_name = 'IStrategy_instrument_id_period'  # 策略实例名称，正常构成形式：class_name_instrument_id_period
    symbol = EMPTY_STRING  # 交易的合约代码
    period = PERIOD_TYPE_TICK
    instrument_id = EMPTY_STRING
    currency = EMPTY_STRING  # 货币（只有IB接口需要）

    # 策略的基本变量，由引擎管理
    _inited = False  # 是否进行了初始化
    _trading = False  # 是否启动交易，由引擎管理
    _running = False  # 是否运行策略，由引擎管理
    pos = 0  # 持仓情况,0＝无持仓，正数＝持多头，负数＝持空头

    # 参数列表，保存了参数的名称
    param_name_list = ['instance_name',  # 策略实例的名称
                       'class_name',  # 策略的类名称
                       'author',
                       'symbol',
                       'buy_cond',
                       'sell_cond',
                       'short_cond',
                       'cover_cond',
                       'trendClass',
                       'slipPoints',
                       'stopLossType',
                       'period',
                       'may_add',
                       'params'
                       ]

    # 变量
    buy_cond = 1  # 看多条件
    sell_cond = 2  # 平多条件
    short_cond = 2  # 看空条件
    cover_cond = 1  # 平空条件

    # 变量列表，保存了变量的名称
    var_list = ['inited',
                'trading',
                'pos',
                'lc',
                'sc',
                'buy',
                'sell',
                'short',
                'cover']
    # 输出列名
    out_columns = ['close', 'buy', 'sell', 'short', 'cover', 'vol', 'trade_price']

    def __init__(self, setting, strategy_event_engine=None, open_direction=OPEN_DIRECTION_BOTH, data_event_engine=None):
        """
        :param strategy_event_engine: 可能是交易用的 StrategyEngine, 也可能是测试用的StrategyTestEngine，
        还可能是回测用的 BacktestEngine
        :param setting:具体策略实例的参数字典{setting_name:default_value},至少要有symbol
        :param open_direction: 可开仓方向
        :param data_event_engine: 数据引擎，提供行情和历数数据的
        """
        self.strategy_class_name = self.__class__.__name__
        self.open_direction = open_direction
        self.strategy_event_engine = strategy_event_engine
        self.data_event_engine = data_event_engine

        # 当前日期,格式：yyyy-MM-dd 00:00:00, 类型：datetime
        self.today = today_date()
        self.start_datetime = str_datetime(dttm=-30)  # 确定参数（回测）的开始日期时间，默认为当前回溯30天格式：yyyyMMdd 00:00:00  %Y%m%d %H:%M:%S
        self.end_datetime = str_datetime()  # 确定参数（回测）所需数据的结束日期时间，默认为当下，格式：yyyyMMdd 00:00:00

        # 策略基础周期
        self.baseperiod = PERIOD_TYPE_TICK
        self.params = {}  # 计算指标用到的参数列表，key为参数名称，值为：StrategyParam

        # 策略滑点
        self.slippage = 0
        # 策略当前信号
        self.current_strategy_signal = StrategySignal()
        self.strategy_signals_count = EMPTY_INT
        self.out_data = pd.DataFrame(columns=self.out_columns)

        # 策略信号序列字典，日期时间是Key,策略信号是Value
        self.strategy_signals = {}
        self.buy_series = pd.Series()  # 开多仓信号序列
        self.sell_series = pd.Series()  # 平多仓信号序列
        self.short_series = pd.Series()  # 开空仓信号序列
        self.cover_series = pd.Series()  # 平空仓信号序列

        self.may_buy = True  # 当前状态，可以开多或者平空
        self.may_sell = True  # 当前状态，可以开空或者平多
        self.may_add = False  # 当前状态，可加仓
        self.symbol = ''
        if setting:
            if 'symbol' in setting:
                self.symbol = setting['symbol']
            d = self.__dict__
            # print(d)
            for key in self.param_name_list:
                if key in setting.keys():
                    d[key] = setting[key]
            self.instance_name = '_'.join([self.strategy_class_name, self.symbol, str(self.baseperiod)])

    def register(self, data_event_engine):
        self.data_event_engine = data_event_engine
        data_event_engine.register(EVENT_RSP_TICKS + self.symbol, self.on_ticks)
        data_event_engine.register(EVENT_RSP_BARS + self.symbol, self.on_bars)

    def set_instrument_id(self, instrument_id):
        """设置交易标的"""
        self.instrument_id = instrument_id
        self.symbol = instrument_id
        self.instance_name = '_'.join([self.strategy_class_name, self.symbol, str(self.baseperiod)])

    def set_strategy_engine(self, strategy_event_engine):
        self.strategy_event_engine = strategy_event_engine

    def set_pos(self, pos):
        """
        设置持仓，<0:空仓，>0: 多仓，=0：持币，实盘时用，
        :param pos:
        :return:
        """
        self.pos = pos
        if pos == 0:
            self.may_buy = True
            self.may_sell = True
        elif pos > 0:
            self.may_sell = True
            self.may_buy = False
            if self.may_add:
                self.may_buy = True
        else:
            self.may_buy = True
            self.may_sell = False
            if self.may_add:
                self.may_sell = True

    @property
    def name(self):
        """设置并返回策略实例名称"""
        return self.instance_name

    def on_init(self, params=None):
        """初始化策略，必须由用户继承实现,并要调用此父函数
        调用此函数前要保证 self.strategy_engine is not None
        在此可以调入参数及历史数据，优化参数，设置止损方法
        根据选中的参数及历史数据计算指标值
        """
        if self._inited:
            self.req_log('请勿重复初始化策略实例：%s' % self.instance_name)
            return

        self._inited = True
        # 调入参数
        if params:
            self.params = params

        # 优化参数

        # 设置止损方法

        # 调入历史数据，计算相关指标：对于回测同时计算策略信号

        # print('strategyTemplate', self.symbol)

    def on_start(self):
        """启动策略（必须由用户继承实现）
        Start the order-tick-k-bar-data-event and
         the calculate-indicators-event to
        define the new strategic signal
        """
        # 保证启动前策略已经初始化
        if not self._inited:
            self.on_init()
        if self._running:
            return
        else:
            self._running = True

        # 注册行情事件
        # if self.symbol:
        #     self.strategy_engine.add_strategy_symbol(self.symbol)
        #     self.data_engine.register(EVENT_TICK + self.symbol, self.on_tick)
        #     self.data_engine.register(EVENT_BAR + self.symbol, self.on_bar)

    def on_stop(self):
        """停止策略（必须由用户继承实现）

        """
        self._running = False

        # 注销事件
        # self.data_event_engine.unregister(EVENT_TICK + self.symbol, self.on_tick)

    def on_tick(self, tick):
        """收到行情TICK推送（必须由用户继承实现）
        利用新的tick数据，计算相关指标，并判断是否有交易信号产生"""
        raise NotImplementedError

    def on_ticks(self, ticks):
        """收到历史行情TICKs推送,used by backtest
        （必须由用户继承实现）"""
        raise NotImplementedError

    def on_tick_event(self, event):
        """ called  by data_server or BarDataFrame """
        tick = event.dict_['data']
        self.on_tick(tick)

    def on_ticks_event(self, event):
        """ called  by BarDataFrame"""
        ticks = event.dict_['data']
        self.on_ticks(ticks)

    def on_order(self, order):
        """收到委托变化推送（必须由用户继承实现）"""
        raise NotImplementedError

    def on_trade(self, event):
        """收到成交推送（必须由用户继承实现）"""
        raise NotImplementedError

    def on_bar(self, bar):
        """收到Bar推送（必须由用户继承实现）"""
        raise NotImplementedError

    def on_bars(self, bars):
        """收到历史Bars推送（必须由用户继承实现）
        used by backtest
        计算所有用到的指标
        """
        raise NotImplementedError

    def on_bar_event(self, event):
        """收到Bar推送 """
        bar=event.dict_['data']
        self.on_bar(bar)

    def on_bars_event(self, event):
        """收到历史Bars推送
        used by backtest
        计算所有用到的指标
        """
        bars = event.dict_['data']
        self.on_bars(bars)

    def buy(self, price, volume, stop_price=EMPTY_FLOAT, stop=False, dt=datetime.datetime.now()):
        """买开=OpenLong"""
        # self.strategyEngine.openLong(self.vtSymbol, price, volume)
        self.send_order(STRATEGY_ORDER_BUY, price, volume, stop_price, stop, dt)
        self.set_pos(self.pos + volume)

    def sell(self, price, volume, stop_price=EMPTY_FLOAT, stop=False, dt=datetime.datetime.now()):
        """卖平=CloseLong"""
        # self.strategyEngine.closeLong(self.vtSymbol, price, volume)
        self.send_order(STRATEGY_ORDER_SELL, price, volume, stop_price, stop, dt)
        self.set_pos(self.pos - volume)

    def short(self, price, volume, stop_price=EMPTY_FLOAT, stop=False, dt=datetime.datetime.now()):
        """卖开=OpenShort"""
        # self.strategyEngine.openShort(self.vtSymbol, price, volume)
        self.send_order(STRATEGY_ORDER_SHORT, price, volume, stop_price, stop, dt)
        self.set_pos(self.pos - volume)

    def cover(self, price, volume, stop_price=EMPTY_FLOAT, stop=False, dt=datetime.datetime.now()):
        """买平=CloseShort"""
        # self.strategyEngine.closeShort(self.vtSymbol, price, volume)
        self.send_order(STRATEGY_ORDER_COVER, price, volume, stop_price, stop, dt)
        self.set_pos(self.pos + volume)

    def send_order(self, order_type, price, volume, stop_price=EMPTY_FLOAT, stop=False, dt=datetime.datetime.now()):
        # print("""发送委托 from strategy""")
        ss = StrategySignal()
        ss.datetime = dt
        ss.symbol = self.symbol
        ss.name = self.instance_name
        ss.price = price
        ss.vol = volume
        ss.order_type = order_type
        ss.params = self.params
        ss.stop_price = stop_price

        now = datetime.now().strftime('%Y%m%d %H:%M:%S.%f')
        self.strategy_signals[now] = ss
        event = Event(event_type=EVENT_STRATEGY_SIGNAL + self.instance_name, dict_={'data': ss})

        self.strategy_event_engine.put(event)

    def cancel_order(self, vtOrderID):
        """撤单"""
        # 如果发单号为空字符串，则不进行后续操作
        if not vtOrderID:
            return

        if STOPORDERPREFIX in vtOrderID:
            self.strategy_engine.cancelStopOrder(vtOrderID)
        else:
            self.strategy_engine.delete_order(vtOrderID)

    def rq_params(self):
        """读取策略参数
        参数放在 config/strategy_params.json中
        以策略类名、实例名为key"""
        # print(self.className, self.symbol)
        event = Event(event_type=EVENT_REQ_PARAMS, dict_={'data': self.instance_name})
        self.data_event_engine.put(event)
        # self.params = self.strategy_engine.get_params(self.instance_name)

    def rq_ticks(self, start_datetime, end_datetime, days=10):
        """读取tick数据"""
        # self.ticks = self.strategy_engine.load_tick(self.symbol, days=days)
        # self.on_ticks(self.ticks)
        event = Event(event_type=EVENT_REQ_TICKS, dict_={'symbol': self.symbol,
                                                         'start_datetime': self.start_datetime,
                                                         'end_datetime': self.end_datetime})
        self.data_event_engine.put(event)

    def req_log(self, content):
        """记录CTA日志"""
        content = self.instance_name + ':' + content
        self.strategy_event_engine.req_log(content)

    def put_event(self):
        """发出策略状态变化事件"""
        self.strategy_event_engine.put(self.instance_name)

    def get_engine_type(self):
        """查询当前运行的环境:trading  or backtesting"""
        return self.strategy_engine.engineType

    def calc_signal(self, data, params):
        """
        计算策略信号，由具体策略实现
        :param data:pd.DataFrame 计算信号要用到的数据，如果是 Tick 类数据，包含datetime,lastPrice,volume;
        如果是Bar类数据，包含datetime,open,high,low,close,volume
        :param params: 计算信号用到的参数字典，参数名称：值
        :return:pd.DataFrame, 在data中增加：buy,sell, short, cover 标志列
        """

    def calc_result(self, data, long_short=0):
        """
        回测时用，计算策略的统计结果
        :param data:交易序列，类型：pd.DataFrame,包含日期时间（datetime)、收盘价格（close）、
                    交易价格(price)、开多(buy)、平多(sell)、开空(short)、平空(cover)、交易量(vol)
        :param long_short: 1=long,-1=short,0=both
        :return:
        """
        rslt = 1

        return data

    def run_optimization(self, data, optimizationSetting):
        """优化参数"""
        # 获取优化设置
        settingList = optimizationSetting.generate_setting()
        targetName = optimizationSetting.optimizeTarget

        # 检查参数设置问题
        if not settingList or not targetName:
            self.output('优化设置有问题，请检查')

        # 遍历优化
        resultList = []
        for setting in settingList:
            self.clearBacktestingResult()
            self.output('-' * 30)
            self.output('setting: %s' % str(setting))
            self.calc_signal(data, setting)
            d = self.calc_result(data)
            try:
                targetValue = d[targetName]
            except KeyError:
                targetValue = 0
            resultList.append(([str(setting)], targetValue))

        # 显示结果
        resultList.sort(reverse=True, key=lambda result: result[1])
        self.output('-' * 30)
        self.output('优化结果：')
        for result in resultList:
            self.output('%s: %s' % (result[0], result[1]))

    def clearBacktestingResult(self):
        """清空之前回测的结果"""
        # 清空限价单相关
        self.limitOrderCount = 0
        self.limitOrderDict.clear()
        self.workingLimitOrderDict.clear()

        # 清空停止单相关
        self.stopOrderCount = 0
        self.stopOrderDict.clear()
        self.workingStopOrderDict.clear()

        # 清空成交相关
        self.tradeCount = 0
        self.tradeDict.clear()

    def run_parallel_optimization(self, data, optimizationSetting):
        """并行优化参数"""
        # 获取优化设置
        settingList = optimizationSetting.generate_setting()
        targetName = optimizationSetting.optimizeTarget

        # 检查参数设置问题
        if not settingList or not targetName:
            self.output('优化设置有问题，请检查')

        # 多进程优化，启动一个对应CPU核心数量的进程池
        pool = multiprocessing.Pool(multiprocessing.cpu_count())
        l = []

        for setting in settingList:
            l.append(pool.apply_async(self.on_optimize, (self, setting,
                                                         targetName, self.mode,
                                                         self.start_datetime, self.initDays, self.end_datetime,
                                                         self.slippage, self.rate, self.size,
                                                         self.dbName, self.symbol)))
        pool.close()
        # pool.join()

        # 显示结果
        resultList = [res.get() for res in l]
        resultList.sort(reverse=True, key=lambda result: result[1])
        self.output('-' * 30)
        self.output(u'优化结果：')
        for result in resultList:
            self.output(u'%s: %s' % (result[0], result[1]))

    def on_optimize(strategyClass, setting, targetName,
                    mode, startDate, initDays, endDate,
                    slippage, rate, size,
                    dbName, symbol):
        """多进程优化时跑在每个进程中运行的函数"""
        # account_engine = TradeEngine()
        # engine = BacktestEngine(account_engine)
        # engine.setBacktestingMode(mode)
        # engine.setStartDate(startDate, initDays)
        # engine.setSlippage(slippage)
        # engine.setRate(rate)
        # engine.setSize(size)
        # engine.setDatabase(dbName, symbol)
        #
        # engine.initStrategy(strategyClass, setting)
        # engine.runBacktesting()
        # d = engine.calculateBacktestingResult()
        # try:
        #     targetValue = d[targetName]
        # except KeyError:
        #     targetValue = 0
        # return (str(setting), targetValue)

    def output(self, msg):
        if self.strategy_event_engine:
            self.req_log(msg)


if __name__ == '__main__':
    setting = {
        "name": "DEMA_Ni_M1",
        "className": "DoubleEmaDemo",
        "symbol": "ni1801",
        "vtSymbol": "ni1801",
        "baseperiod": 1}

    # data_event_engine = EventEngine()
    # strategy_event_engine = IStrategyEngine(data_engine=data_event_engine)

    # stt1 = StrategyFractal(strategy_event_engine, setting)
    # stt2 = StrategyTrend(strategy_event_engine, setting)
